﻿using Domain;
using Storage.DbEntities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common.Extensions;

namespace Storage.Accessors
{
	public class ExerciseAccessor
	{
		private Repositories.ExerciseRep _exerciseRep;
		private Repositories.ExerciseLogRep _exerciseLogRep;
		private Repositories.SetLogRep _setLogRep;

		public ExerciseAccessor(Repositories.ExerciseRep exerciseRep, Repositories.ExerciseLogRep exerciseLogRep, Repositories.SetLogRep setLogRep)
		{
			_exerciseRep = exerciseRep;
			_exerciseLogRep = exerciseLogRep;
			_setLogRep = setLogRep;
		}

		public async Task Clear()
		{
			await _exerciseRep.Clear();
			await _exerciseLogRep.Clear();
			await _setLogRep.Clear();
		}

        public async Task<ExerciseLookup> GetExerciseLookup(Guid exerciseId)
        {
            var exerciseDbEntity = await _exerciseRep.GetByIdAsync(exerciseId);            
            var dbExerciseLogs = (await _exerciseLogRep.GetByExerciseId(exerciseId)).OrderBy(x => x.LogDate).ToList();
            var dbSetLogs = (await _setLogRep.GetByExerciseLogIds(dbExerciseLogs.Select(x => x.Id))).OrderBy(x => x.Weight).ToList();

            var lastExerciseLog = dbExerciseLogs.Return(x => x.LastOrDefault(), new ExerciseLogDbEntity());


            return new ExerciseLookup() 
            {
                Date = lastExerciseLog.LogDate,
                DisplayName = exerciseDbEntity.Return(x => x.DisplayName, null),
                ExerciseId = exerciseId,
                RecordWeight = dbSetLogs.With(x => x.LastOrDefault()).Return(x => x.Weight, 0.0),
                MaxWeight = dbSetLogs.With(x => x.Where(y => y.FK_ExerciseLogId == lastExerciseLog.Id)).With(x => x.OrderBy(y => y.Weight)).Return(x => x.Last().Weight, 0),
            };
        }

		public async Task<List<ExerciseLookup>> GetExerciseLookups()
		{
			var dbExercises = await _exerciseRep.GetAll();
			var dbExerciseLogs = (await _exerciseLogRep.GetAll());
			var dbSetLogs = (await _setLogRep.GetAll());

			var joined = dbExercises
				.Join(dbExerciseLogs, dbExercise => dbExercise.Id, dbExerciseLog => dbExerciseLog.FK_ExerciseId, (x, y) => new 
				{ 
					ExerciseId = x.Id, 
					ExerciseDisplayName = x.DisplayName,
					ExerciseLogId = y.Id,
					ExerciseLogDate = y.LogDate,
				})
				.Join(dbSetLogs, x => x.ExerciseLogId, dbSetLog => dbSetLog.FK_ExerciseLogId, (x, y) => new
				{
					ExerciseId = x.ExerciseId,
					ExerciseDisplayName = x.ExerciseDisplayName,
					ExerciseLogDate = x.ExerciseLogDate,
					Reps = y.Repetitions,
					Weight = y.Weight,
					SetEndTime = y.EndDateTime,
				})
				.ToLookup(x => x.ExerciseId);

			return dbExercises.Select(dbExercise => 
			{
				if (!joined.Contains(dbExercise.Id))
					return new ExerciseLookup() { ExerciseId = dbExercise.Id, DisplayName = dbExercise.DisplayName };

				var lastLogDate = joined[dbExercise.Id].Max(x => x.ExerciseLogDate);
				var lastWeight = joined[dbExercise.Id].Where(x => x.ExerciseLogDate == lastLogDate).Max(x => x.Weight);

				var res = new ExerciseLookup()
					{
						DisplayName = dbExercise.DisplayName,
						ExerciseId = dbExercise.Id,
						Date = lastLogDate,
						RecordWeight = joined[dbExercise.Id].Max(x => x.Weight),
						MaxWeight = lastWeight
					};
				return res;
			}).ToList();
		}

		public async Task<List<Exercise>> GetAll()
		{
			var dbExercises = await _exerciseRep.GetAll();

			var dbExerciseLogs = await _exerciseLogRep.GetAll();
			var dbSetLogs = await _setLogRep.GetAll();

			return FromDb(dbExercises, dbExerciseLogs, dbSetLogs);
		}

		private static List<Exercise> FromDb(List<ExerciseDbEntity> dbExercises, List<ExerciseLogDbEntity> dbExerciseLogs, List<SetLogDbEntity> dbSetLogs)
		{
			var groupedDbExerciseLogs = dbExerciseLogs.ToLookup(x => x.FK_ExerciseId);
			var groupedDbSetLogs = dbSetLogs.ToLookup(x => x.FK_ExerciseLogId);

			var res = dbExercises.Select(dbExercise => new Exercise()
			{
				Id = dbExercise.Id,
				DisplayName = dbExercise.DisplayName,
				ExerciseLogs = groupedDbExerciseLogs[dbExercise.Id].Select(dbExerciseLog => ExerciseLogFromDbEntity(dbExerciseLog, groupedDbSetLogs[dbExerciseLog.Id])).ToList()
			}).ToList();
			return res;
		}

		private static ExerciseLog ExerciseLogFromDbEntity(ExerciseLogDbEntity dbExerciseLog, IEnumerable<SetLogDbEntity> setLogDbEntitys)
		{
			return new ExerciseLog()
			{
				Id = dbExerciseLog.Id,
				LogDate = dbExerciseLog.LogDate,
				Sets = setLogDbEntitys.Return(x => x.Select(y => SetLogFromDbEntity(y)).ToList(), new List<SetLog>())
			};
		}

		private static SetLog SetLogFromDbEntity(SetLogDbEntity dbSetLog)
		{
			return new SetLog()
			{
				Id = dbSetLog.Id,
				EndDateTime = dbSetLog.EndDateTime,
				Repetitions = dbSetLog.Repetitions,
				Weight = dbSetLog.Weight
			};
		}

		public async Task SaveAll(IEnumerable<Exercise> exercises)
		{
			if (exercises == null)
				return;

			foreach (var exercise in exercises)
			{
				await SaveExerciseAsync(exercise);
			}
		}

		public async Task SaveExerciseAsync(Exercise exercise)
		{
			if (exercise == null) return;

			var lastLog = exercise.GetLastLog();
			var dbExercise = new DbEntities.ExerciseDbEntity()
			{
				Id = exercise.Id,
				DisplayName = exercise.DisplayName
			};
			await _exerciseRep.SaveAsync(dbExercise);
			exercise.Id = dbExercise.Id;

			await SaveExerciseLogsAsync(exercise);
		}

		public async Task SaveExerciseLogsAsync(Exercise exercise)
		{
			if (exercise == null || exercise.ExerciseLogs == null)
				return;

			foreach (var exerciseLog in exercise.ExerciseLogs)
			{
				await SaveExerciseLogAsync(exercise.Id, exerciseLog);
			}
		}

		public async Task SaveExerciseLogAsync(Guid exerciseId, ExerciseLog exerciseLog)
		{
			if (exerciseLog == null) return;

			var correctExerciseId = await _exerciseRep.Exists(exerciseId);
			if (exerciseId == Guid.Empty)
				throw new ArgumentNullException("Exercise with selected id doesn't exists");

			var dbExerciseLog = new DbEntities.ExerciseLogDbEntity()
			{
				Id = exerciseLog.Id,
				LogDate = exerciseLog.LogDate,
				FK_ExerciseId = exerciseId,
			};
			await _exerciseLogRep.SaveAsync(dbExerciseLog);
			exerciseLog.Id = dbExerciseLog.Id;

			await SaveSetLogsAsync(exerciseLog);
		}

		public async Task SaveSetLogsAsync(ExerciseLog exerciseLog)
		{
			if (exerciseLog == null || exerciseLog.Sets == null)
				return;

			foreach (var setLog in exerciseLog.Sets)
			{
				await SaveSetLogAsync(exerciseLog.Id, setLog);
			}
		}

		public async Task SaveSetLogAsync(Guid exerciseLogId, SetLog setLog)
		{
			if (setLog == null) return;

			if (setLog.Weight < 0.01 || setLog.Repetitions == 0)
				throw new ArgumentOutOfRangeException("setLog is empty");

			var correctExerciseLog = await _exerciseLogRep.GetByExerciseId(exerciseLogId);
			if (correctExerciseLog == null)
				throw new ArgumentNullException("ExerciseLog with selected id doesn't exists");

			var dbSetLog = new DbEntities.SetLogDbEntity()
			{
				Id = setLog.Id,
				FK_ExerciseLogId = exerciseLogId,
				Repetitions = setLog.Repetitions,
				EndDateTime = setLog.EndDateTime,
				Weight = setLog.Weight,
			};

			await _setLogRep.SaveAsync(dbSetLog);
			setLog.Id = dbSetLog.Id;
		}

		public async Task RemoveSetLogAsync(Guid setLogId)
		{
			var dbSetLog = await _setLogRep.GetByIdAsync(setLogId);
			if (dbSetLog == null)
				return;

            await _setLogRep.DeleteById(setLogId);

			var dbExerciseLog = await _exerciseLogRep.GetByIdAsync(dbSetLog.FK_ExerciseLogId);
			var dbSetLogs = await _setLogRep.GetByExerciseLogId(dbExerciseLog.Id);
			if (dbSetLogs == null || dbSetLogs.Count == 0)
			{
				await _exerciseLogRep.Remove(dbExerciseLog);
			}
		}

		public async Task<Boolean> ExerciseExists(String exerciseDisplayName)
		{
			return await _exerciseRep.Exists(exerciseDisplayName);
		}

		public async Task<Boolean> ExerciseExists(Guid exerciseId)
		{
			return await _exerciseRep.Exists(exerciseId);
		}

		public async Task<List<ExerciseLog>> GetExerciseLogsAsync(Guid exerciseId)
		{
			var exerciseExists = await _exerciseRep.Exists(exerciseId);
			if (!exerciseExists)
				throw new ArgumentException();

			var res = new List<ExerciseLog>();

			var dbExerciseLogs = await _exerciseLogRep.GetByExerciseId(exerciseId);
			foreach (var dbExerciseLog in dbExerciseLogs)
			{
				var dbSetLogs = await _setLogRep.GetByExerciseLogId(dbExerciseLog.Id);
				var exerciseLog = ExerciseLogFromDbEntity(dbExerciseLog, dbSetLogs);
				res.Add(exerciseLog);
			}

			return res;
		}

		public async Task<Exercise> GetExerciseByIdAsync(Guid exerciseId)
		{
			var dbExercise = await _exerciseRep.GetByIdAsync(exerciseId);
			var dbExerciseLogs = await _exerciseLogRep.GetByExerciseId(exerciseId);
            var dbSetLogs = await _setLogRep.GetByExerciseLogIds(dbExerciseLogs.Select(x => x.Id));

			var exercise = new Exercise()
			{
				DisplayName = dbExercise.DisplayName,
				Id = dbExercise.Id,
                ExerciseLogs = new List<ExerciseLog>()
			};

			foreach (var dbExerciseLog in dbExerciseLogs)
            {  
				var converted = ExerciseLogFromDbEntity(dbExerciseLog, dbSetLogs.With(x => x.Where(y => y.FK_ExerciseLogId == dbExerciseLog.Id)));
				exercise.ExerciseLogs.Add(converted);
			}

			return exercise;
		}

		public async Task RemoveExerciseAsync(Guid exerciseId)
		{
			var dbExerciseLogs = await _exerciseLogRep.GetByExerciseId(exerciseId);
			
			foreach (var dbExerciseLog in dbExerciseLogs)
			{
				var dbSets = await _setLogRep.GetByExerciseLogId(dbExerciseLog.Id);
				await _setLogRep.RemoveAll(dbSets);
			}
			await _exerciseLogRep.RemoveAll(dbExerciseLogs);
			await _exerciseRep.DeleteById(exerciseId);
		}

		public async Task UpdateExerciseDisplayNameAsync(Guid exerciseId, string displayName)
		{
			await _exerciseRep.UpdateDisplayNameAsync(exerciseId, displayName);
		}

		public async Task UpdateSetLogAsync(SetLog setLog)
		{
			var dbSetLog = await _setLogRep.GetByIdAsync(setLog.Id);
			if (dbSetLog == null)
				return;

			await SaveSetLogAsync(dbSetLog.FK_ExerciseLogId, setLog);
		}
	}
}
